package com.yycx.app.provider.configuration;

import com.yycx.common.annotation.LoginRequired;
import com.yycx.common.base.constants.FlymeConstants;
import com.yycx.common.configuration.OpenCommonProperties;
import com.yycx.common.constants.CommonConstants;
import com.yycx.common.exception.OpenAccessDeniedHandler;
import com.yycx.common.exception.OpenAuthenticationEntryPoint;
import com.yycx.common.security.TokenUtils;
import com.yycx.common.security.enums.TokenStoreEnum;
import com.yycx.common.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.authentication.BearerTokenExtractor;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.expression.OAuth2WebSecurityExpressionHandler;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * oauth2资源服务器配置
 *
 * @author: zyf
 * @date: 2018/10/23 10:31
 * @description:
 */
@Slf4j
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    @Resource
    private RedisConnectionFactory redisConnectionFactory;

    @Resource
    private DataSource dataSource;

    @Resource
    private PasswordEncoder passwordEncoder;

    @Resource
    private OpenCommonProperties openCommonProperties;

    @Resource
    private PermitAllSecurityConfig permitAllSecurityConfig;

    @Resource
    public ApplicationContext applicationContext;

    @Resource
    private RedisUtils redisUtils;

    private BearerTokenExtractor tokenExtractor = new BearerTokenExtractor();
    private OAuth2WebSecurityExpressionHandler expressionHandler;

    @Bean
    public JdbcClientDetailsService clientDetailsService() {
        JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
        jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);
        return jdbcClientDetailsService;
    }

    @Bean
    public OAuth2WebSecurityExpressionHandler oAuth2WebSecurityExpressionHandler(ApplicationContext applicationContext) {
        expressionHandler = new OAuth2WebSecurityExpressionHandler();
        expressionHandler.setApplicationContext(applicationContext);
        return expressionHandler;
    }


    @Bean
    public RedisTokenStore redisTokenStore() {
        return TokenUtils.buildRedisTokenStore(redisConnectionFactory);
    }


    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.headers().frameOptions().disable();
        String[] permitAll = CommonConstants.PERMIT_AUTH;
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers(getPermitAll()).permitAll()
                // 只有拥有actuator权限可执行远程端点
                //.requestMatchers(EndpointRequest.toAnyEndpoint()).hasAnyAuthority(CommonConstants.AUTHORITY_ACTUATOR)
                .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
                //.anyRequest().authenticated()
                //动态权限与authenticated二选一
                .anyRequest().access("@myAuthorizationManager.check(request,authentication)")
                .and()
                .formLogin().loginPage("/login").permitAll()
                .and()
                .apply(permitAllSecurityConfig)
                .and()
                .logout().permitAll()
                // /logout退出清除cookie
                .addLogoutHandler(new CookieClearingLogoutHandler("token", "remember-me"))
                .logoutSuccessHandler(new LogoutSuccessHandler(redisTokenStore(), tokenExtractor))
                .and()
                // 认证鉴权错误处理,为了统一异常处理。每个资源服务器都应该加上。
                .exceptionHandling()
                .accessDeniedHandler(new OpenAccessDeniedHandler())
                .authenticationEntryPoint(new OpenAuthenticationEntryPoint())
                .and()
                .csrf().disable()
                // 禁用httpBasic
                .httpBasic().disable();
    }


    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        String tokenStoreType = FlymeConstants.APP_TOKEN_STORE;
        if (tokenStoreType.equals(TokenStoreEnum.JDBC.name())) {
            resources.tokenServices(TokenUtils.buildJdbcTokenServices(dataSource));
        }
        if (tokenStoreType.equals(TokenStoreEnum.REDIS.name())) {
            resources.tokenServices(TokenUtils.buildRedisTokenServices(redisConnectionFactory));
        }
        if (tokenStoreType.equals(TokenStoreEnum.JWT.name())) {
            resources.tokenServices(TokenUtils.buildRedisTokenServices(redisConnectionFactory));
        }
        // 构建远程获取token,这里是为了支持自定义用户信息转换器
        resources.authenticationEntryPoint(new OpenAuthenticationEntryPoint()).accessDeniedHandler(new OpenAccessDeniedHandler());
        resources.expressionHandler(expressionHandler);
    }

    /**
     * 获取匿名登录地址
     */
    private String[] getPermitAll() {
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = applicationContext.getBean(RequestMappingHandlerMapping.class).getHandlerMethods();
        Set<String> anonymousUrls = new HashSet<>();
        //根据LoginRequired注解获取匿名地址
        for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethods.entrySet()) {
            HandlerMethod handlerMethod = infoEntry.getValue();
            LoginRequired anonymousAccess = handlerMethod.getMethodAnnotation(LoginRequired.class);
            if (anonymousAccess != null) {
                anonymousUrls.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
            }
        }
        //内置匿名地址
        Set<String> permitAllSet = Arrays.stream(CommonConstants.PERMIT_AUTH).collect(Collectors.toSet());
        anonymousUrls.addAll(permitAllSet);
        return anonymousUrls.toArray(new String[anonymousUrls.size()]);
    }
}

